// Copyright 2014 The mqrouter Author. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// TODO: 删除旧版本的 cache sdk
// TODO: 将 redis 文件夹中的 config.go, instance.go, redis.go 都拿到 sdk 根目录下
//       config 和 instance 不应该属于 redis

package xcache

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"gitee.com/go-mid/infra/xcache/xredis"
	"gitee.com/go-mid/infra/xlog"
	"time"
)

// key类型只支持int（包含有无符号，8，16，32，64位）和string
type LoadFunc func(ctx context.Context, key string) (value interface{}, err error)

// CacheValue
type CacheValue struct {
	load  LoadFunc
	cache Cache
	conf  CacheValueConf
}
type CacheValueConf struct {
	Namespace string
	Cluster   string
	Expire    time.Duration
}

// NewCache
func NewCache(cache Cache, conf CacheValueConf, load LoadFunc) *CacheValue {
	return &CacheValue{
		cache: cache,
		load:  load,
		conf:  conf,
	}
}

// Get get value from cache
func (m *CacheValue) GetCacheData(ctx context.Context, key string) (value interface{}, err error) {
	command := "cache.value.SimpleGet"
	WrapMetric(ctx, ValueMetric{
		Command:   command,
		Namespace: m.conf.Namespace,
		Cluster:   m.conf.Cluster,
	}, func() error {
		err = m.getValueFromCache(ctx, key, &value)
		return err
	})
	return
}

// Get get value from cache if miss load value to cache
func (m *CacheValue) Get(ctx context.Context, key string, value interface{}) error {
	fun := "CacheValue.Get -->"
	// TODO 目前统计的是cache层的Get，后面需要拆分为redis层、cache层
	command := "cache.value.Get"
	err := WrapMetric(ctx, ValueMetric{
		Command:   command,
		Namespace: m.conf.Namespace,
		Cluster:   m.conf.Cluster,
	}, func() error {
		err := m.getValueFromCache(ctx, key, value)
		if err == nil {
			statReqHit(ctx, m.conf.Namespace, command, m.conf.Cluster)
			return nil
		}
		if err.Error() != xredis.RedisNil {
			statReqErr(ctx, m.conf.Namespace, command, m.conf.Cluster, err)
			xlog.Errorf(ctx, "%s cache key: %v err: %v", fun, key, err)
			return fmt.Errorf("%s cache key: %v err: %v", fun, key, err)
		}
		statReqMiss(ctx, m.conf.Namespace, command, m.conf.Cluster)

		data, err := m.loadValueToCache(ctx, key)
		if err != nil {
			statReqErr(ctx, m.conf.Namespace, command, m.conf.Cluster, err)
			xlog.Errorf(ctx, "%s loadValueToCache key: %v err: %v", fun, key, err)
			return err
		}

		err = json.Unmarshal(data, value)
		if err != nil {
			statReqErr(ctx, m.conf.Namespace, command, m.conf.Cluster, err)
			return errors.New(string(data))
		}
		return err
	})
	return err
}

// Del ...
func (m *CacheValue) Del(ctx context.Context, key string) error {
	command := "cache.value.Del"

	err := WrapMetric(ctx, ValueMetric{
		Command:   command,
		Namespace: m.conf.Namespace,
		Cluster:   m.conf.Cluster,
	}, func() error {
		_, err := m.cache.Del(ctx, key)
		return err
	})

	return err
}

// Load load value and set to cache
func (m *CacheValue) Load(ctx context.Context, key string) error {
	command := "cache.value.Load"
	err := WrapMetric(ctx, ValueMetric{
		Command:   command,
		Namespace: m.conf.Namespace,
		Cluster:   m.conf.Cluster,
	}, func() error {
		_, err := m.loadValueToCache(ctx, key)
		return err
	})
	return err
}

func (m *CacheValue) getValueFromCache(ctx context.Context, key string, value interface{}) error {
	data, err := m.cache.Get(ctx, key)
	if err != nil {
		return err
	}
	dataStr, ok := data.(string)
	if !ok {
		mData, _ := json.Marshal(data)
		dataStr = string(mData)
	}
	err = json.Unmarshal([]byte(dataStr), value)
	if err != nil {
		return err
	}
	return nil
}

func (m *CacheValue) loadValueToCache(ctx context.Context, key string) (data []byte, err error) {
	fun := "CacheValue.loadValueToCache -->"
	expire := m.conf.Expire

	value, err := m.load(ctx, key)
	if err != nil {
		xlog.Warnf(ctx, "%s load err, cache key:%v err:%v", fun, key, err)
		data = []byte(`{}`)
		expire = xredis.CacheDirtyExpireTime

	} else {
		data, err = json.Marshal(value)
		if err != nil {
			xlog.Errorf(ctx, "%s marshal err, cache key:%v err:%v", fun, key, err)
			data = []byte(`{}`)
			expire = xredis.CacheDirtyExpireTime
		}
	}
	_, err = m.cache.Set(ctx, key, data, expire)
	if err != nil {
		xlog.Errorf(ctx, "%s set err, cache key:%v rerr:%v", fun, key, err)
		return nil, err
	}
	return data, nil
}
